package com.itranswarp.weibo.api;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.yaml.snakeyaml.Yaml;

/**
 * Code generator.
 * 
 * @author Michael Liao (askxuefeng@gmail.com)
 */
public class Main {

	static final String VERSION = "1.0";

	public static void main(String[] args) throws Exception {
		String classPath = System.getProperty("java.class.path");
		String[] paths = classPath.split("\\" + File.pathSeparator);
		String MainClassPath = Main.class.getName().replace('.', File.separatorChar) + ".class";
		String currentPath = null;
		for (String path : paths) {
			if (new File(path + File.separatorChar + MainClassPath).exists()) {
				currentPath = path;
				break;
			}
		}
		System.out.println("bin path: " + currentPath);
		File sourcePath = new File(new File(currentPath).getParentFile().getPath() + File.separatorChar + "src" + File.separatorChar + Main.class.getPackage().getName().replace('.', File.separatorChar));
		System.out.println("src path: " + sourcePath.getPath());
		parse(sourcePath);
	}

	@SuppressWarnings("unchecked")
	public static void parse(File writeToDir) throws Exception {
		InputStream input = Main.class.getClassLoader().getResourceAsStream("weibo.yaml");
		Reader reader = new BufferedReader(new InputStreamReader(input, "UTF-8"));
		Yaml yaml = new Yaml();
		List<Map<String, Object>> list = (List<Map<String, Object>>) yaml.load(reader);
		List<ApiCode> apiCodes = new LinkedList<ApiCode>();
		for (Map<String, Object> map : list) {
			ApiCode apiCode = new ApiCode(map);
			apiCodes.add(apiCode);
		}
		String time = new SimpleDateFormat("yyyy-MM-dd HH:mm").format(System.currentTimeMillis());
		String clientFile = createApiClient(apiCodes);
		writeFile(writeToDir, "ApiClient.java", time, clientFile);
		String loaderFile = createApiLoader(apiCodes);
		writeFile(writeToDir, "ApiLoader.java", time, loaderFile);
	}

	static String toJavaName(String apiName) {
		String[] ss = apiName.split("\\/|\\_");
		if (ss.length==1)
			return apiName;
		StringBuilder sb = new StringBuilder(64).append(ss[0]);
		for (int i=1; i<ss.length; i++) {
			sb.append(Character.toUpperCase(ss[i].charAt(0))).append(ss[i].substring(1));
		}
		return sb.toString();
	}

	static void writeFile(File writeToDir, String fileName, String time, String fileContent) throws IOException {
		File file = new File(writeToDir, fileName);
		System.out.println("Write to: " + file.getPath());
		Writer writer = null;
		try {
			writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), "UTF-8"));
			writer.write(fileContent.replace("${TIME}", time).replace("${VERSION}", VERSION));
		}
		finally {
			if (writer!=null) {
				try { writer.close(); } catch (IOException e) {}
			}
		}
	}

	static String createApiClient(List<ApiCode> list) {
		List<ApiCode> apiCodes = new LinkedList<ApiCode>();
		for (ApiCode apiCode : list) {
			apiCodes.add(apiCode);
			if ("UPLOAD".equals(apiCode.method))
				apiCodes.add(new ApiCode(apiCode));
		}
		StringBuilder sb = new StringBuilder(4096)
			.append("package com.itranswarp.weibo.api;\n\n")
			.append("/**\n")
			.append(" * Sina Weibo OAuth 2 Java client interface. \n")
			.append(" * Created: ${TIME}\n")
			.append(" * \n")
			.append(" * NOTE: this interface is auto-generated, \n")
			.append(" *       please DO NOT modify it.\n")
			.append(" * \n")
			.append(" * @version ${VERSION}\n")
			.append(" */\n")
			.append("public interface ApiClient {\n");
		for (ApiCode api : apiCodes) {
			for (int n=api.minimumArgs; n<=api.args.size(); n++) {
				sb.append("\n").append(api.toJavaString(n));
			}
		}
		sb.append("}\n");
		return sb.toString();
	}

	static String createApiLoader(List<ApiCode> apiCodes) {
		StringBuilder sb = new StringBuilder(1024)
		  .append("package com.itranswarp.weibo.api;\n\n")
		  .append("/**\n")
		  .append(" * API Loader. \n")
		  .append(" * Created: ${TIME}\n")
		  .append(" * \n")
		  .append(" * NOTE: this interface is auto-generated, \n")
		  .append(" *       please DO NOT modify it.\n")
		  .append(" * \n")
		  .append(" * @version ${VERSION}\n")
		  .append(" */\n")
		  .append("class ApiLoader {\n")
		  .append("    static java.util.Map<String, ApiInvocation> initApiMap() {\n")
		  .append("        java.util.Map<String, ApiInvocation> apiMap = new java.util.HashMap<String, ApiInvocation>();\n");
		for (ApiCode api : apiCodes) {
			String params = api.toApiParams();
			sb.append("        apiMap.put(\"" + Main.toJavaName(api.api) + "\", new ApiInvocation(HttpMethod." + api.method + ", \"" + api.api + "\"" + params + "));\n");
		}
		sb.append("        return apiMap;\n")
		  .append("    }\n")
		  .append("}\n");
		return sb.toString();
	}

	static void print(Object o) {
		System.out.println(o);
	}
}

class ApiCode {

	final Log log = LogFactory.getLog(getClass());

	final String api;
	final String description;
	final String method;
	final List<ArgCode> args;
	final int minimumArgs;

	public ApiCode(ApiCode apiCode) {
		this.api = apiCode.api;
		this.description = apiCode.description;
		this.method = apiCode.method;
		this.minimumArgs = apiCode.minimumArgs;
		this.args = new LinkedList<ArgCode>();
		for (ArgCode argCode : apiCode.args)
			args.add(new ArgCode(argCode));
	}

	String toApiParams() {
		if (args.isEmpty())
			return "";
		StringBuilder sb = new StringBuilder(64);
		for (ArgCode arg : args) {
			sb.append(", ").append(arg.toApiParam());
		}
		return sb.toString();
	}

	public ApiCode(Map<String, Object> map) {
		api = (String) map.get("api");
		description = (String) map.get("description");
		String httpMethod = ((String) map.get("method")).toUpperCase();
		if ( ! httpMethod.equals("GET") && ! httpMethod.equals("POST"))
			throw new RuntimeException("Method must be get or post.");
		@SuppressWarnings("unchecked")
		Map<String, Object> argsMap = (Map<String, Object>) map.get("args");
		if (argsMap==null)
			argsMap = Collections.emptyMap();
		args = new LinkedList<ArgCode>();
		boolean multipart = false;
		int minArgs = 0;
		boolean optional = false;
		for (String key : argsMap.keySet()) {
			String value = (String) argsMap.get(key);
			ArgCode argCode = new ArgCode(key, value);
			multipart = multipart | File.class.equals(argCode.type);
			if ( ! argCode.optional) {
				if (optional)
					throw new RuntimeException("Optional args must be after required args.");
				minArgs ++;
			}
			else
				optional = true;
			args.add(argCode);
		}
		if (multipart) {
			if ("GET".equals(httpMethod))
				throw new RuntimeException("Cannot use GET when has file argument.");
			httpMethod = "UPLOAD";
		}
		this.method = httpMethod;
		this.minimumArgs = minArgs;
	}

	public String toJavaString(int n) {
		StringBuilder sb = new StringBuilder(1024)
			.append("    /**\n")
			.append("     * API: ").append(api).append("\n")
			.append("     * ").append(description).append("\n")
			.append("     * \n");
		for (int i=0; i<n; i++) {
			ArgCode arg = args.get(i);
			sb.append("     * @param ").append(arg.toJavaDoc()).append("\n");
		}
		sb  .append("     * @return 有效的JSON对象\n")
			.append("     */\n")
			.append("    <T> T ").append(Main.toJavaName(api)).append("(")
			.append(buildArgs(n))
			.append(");\n");
		return sb.toString();
	}

	String buildArgs(int n) {
		List<ArgCode> list = args.subList(0, n);
		if (list.isEmpty())
			return "";
		StringBuilder sb = new StringBuilder(128);
		for (ArgCode arg : list) {
			sb.append(arg.toArgString()).append(", ");
		}
		return sb.substring(0, sb.length() - 2);
	}
}

class ArgCode {

	static final Log log = LogFactory.getLog(ArgCode.class);

	static final String TYPES;
	static final Map<String, ArgType<?>> typeMap = new HashMap<String, ArgType<?>>();
	static {
		ArgType<Integer> argInt = new ArgType<Integer>(Integer.class) {
			Object parse(String value) {
				if (value==null || value.equals("null"))
					return null;
				return Integer.parseInt(value);
		}};
		ArgType<Long> argLong = new ArgType<Long>(Long.class) {
			Object parse(String value) {
				if (value==null || value.equals("null"))
					return null;
				return Long.parseLong(value);
		}};
		ArgType<Float> argFloat = new ArgType<Float>(Float.class) {
			Object parse(String value) {
				if (value==null || value.equals("null"))
					return null;
				return Float.parseFloat(value);
		}};
		ArgType<String> argString = new ArgType<String>(String.class) {
			Object parse(String value) {
				return (value==null || value.equals("null")) ? null : value.trim();
		}};
		ArgType<File> argFile = new ArgType<File>(File.class) {
			Object parse(String value) {
				if (value==null || value.equals("null"))
					return null;
				throw new RuntimeException("Default value of file must be null.");
		}};
		typeMap.put("string", argString);
		typeMap.put("int", argInt);
		typeMap.put("int64", argLong);
		typeMap.put("long", argLong);
		typeMap.put("float", argFloat);
		typeMap.put("file", argFile);
		String types = "";
		for (String key : typeMap.keySet())
			types = types + "|" + key;
		TYPES = types.substring(1);
	}

	// name
	static final Pattern P_NAME = Pattern.compile("^\\w+$");

	// type
	static final Pattern P_TYPE_1 = Pattern.compile("^\\s*(" + TYPES + ")\\s*$", Pattern.CASE_INSENSITIVE);

	// type(default)
	static final Pattern P_TYPE_2 = Pattern.compile("^\\s*(" + TYPES + ")\\s*\\(\\s*(\\w+)\\s*\\)\\s*$", Pattern.CASE_INSENSITIVE);

	public ArgCode(String name, String value) {
		if ( ! P_NAME.matcher(name).matches())
			throw new RuntimeException("Invalid name: " + name);
		this.name = name;
		String[] ss = value.split("\\,", 2);
		String s = ss[0];
		this.description = ss.length==2 ? ss[1] : "";
		Matcher m = P_TYPE_1.matcher(s);
		ArgType<?> argType = null;
		if (m.matches()) {
			argType = typeMap.get(m.group(1).toLowerCase());
			if (argType==null)
				throw new RuntimeException("Invalid type: " + m.group(1));
			this.optional = false;
			this.defaultValue = null;
		}
		else {
			m = P_TYPE_2.matcher(s);
			if (m.matches()) {
				argType = typeMap.get(m.group(1).toLowerCase());
				if (argType==null)
					throw new RuntimeException("Invalid type: " + m.group(1));
				this.optional = true;
				this.defaultValue = argType.parse(m.group(2));
			}
			else {
				throw new RuntimeException("Cannot parse arg value: " + value);
			}
		}
		this.type = argType.clazz;
	}

	String toApiParam() {
		String s = "new ApiParam(\"" + name + "\", ";
		String jtype = type.getName();
		if (jtype.startsWith("java.lang."))
			jtype = jtype.substring(10);
		s = s + jtype + ".class";
		if (optional) {
			String v = String.valueOf(defaultValue);
			if (String.class.equals(type) && defaultValue!=null)
				v = "\"" + v + "\"";
			if (Long.class.equals(type) && defaultValue!=null)
				v = v + "L";
			if (Float.class.equals(type) && defaultValue!=null)
				v = v + "F";
			s = s + ", " + v;
		}
		s = s + ")";
		return s;
	}

	String name;
	Class<?> type;
	boolean optional;
	Object defaultValue;
	String description;

	public ArgCode(ArgCode argCode) {
		this.name = argCode.name;
		this.optional = argCode.optional;
		this.defaultValue = argCode.defaultValue;
		this.description = argCode.description;
		this.type = File.class.equals(argCode.type) ? InputStream.class : argCode.type;
	}

	public String toArgString() {
		return new StringBuilder(32)
				.append(toJavaType())
				.append(" ")
				.append(toArgName())
				.toString();
	}

	String toJavaType() {
		String className = this.type.getName();
		if (className.startsWith("java.lang."))
			className = className.substring(10);
		return className;
	}

	String toArgName() {
		return "p" + Character.toUpperCase(name.charAt(0)) + name.substring(1);
	}

	public String toJavaDoc() {
		return name + " " + (optional ? "默认：" + defaultValue + "，" : "必选，") + description;
	}
}

abstract class ArgType<E> {

	Class<E> clazz;

	public ArgType(Class<E> clazz) {
		this.clazz = clazz;
	}

	abstract Object parse(String value);
	
}
